2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIContactStatusDockOverlaysPlugin.h"
18 #import <Adium/AIChatControllerProtocol.h>
19 #import <Adium/AIContactControllerProtocol.h>
20 #import <Adium/AIContentControllerProtocol.h>
21 #import "AIDockController.h"
22 #import <Adium/AIInterfaceControllerProtocol.h>
23 #import <Adium/AIPreferenceControllerProtocol.h>
24 #import <Adium/AIContactAlertsControllerProtocol.h>
25 #import <AIUtilities/AIColorAdditions.h>
26 #import <AIUtilities/AIDictionaryAdditions.h>
27 #import <AIUtilities/AIParagraphStyleAdditions.h>
28 #import <AIUtilities/AIArrayAdditions.h>
29 #import <AIUtilities/AIImageAdditions.h>
30 #import <Adium/AIAbstractListController.h>
31 #import <Adium/AIAccount.h>
32 #import <Adium/AIChat.h>
33 #import <Adium/AIIconState.h>
35 #define SMALLESTRADIUS 15
36 #define RADIUSRANGE 36
37 #define SMALLESTFONTSIZE 14
38 #define FONTSIZERANGE 30
40 #define DOCK_OVERLAY_ALERT_SHORT AILocalizedString(@"Display name in the dock icon",nil)
41 #define DOCK_OVERLAY_ALERT_LONG DOCK_OVERLAY_ALERT_SHORT
43 @interface AIContactStatusDockOverlaysPlugin (PRIVATE)
45 - (NSImage *)overlayImageFlash:(BOOL)flash;
46 - (void)preferencesChanged:(NSNotification *)notification;
47 - (void)flushPreferenceColorCache;
50 @implementation AIContactStatusDockOverlaysPlugin
57 overlayObjectsArray = [[NSMutableArray alloc] init];
60 //Register as a contact observer (For signed on / signed off)
61 [[adium contactController] registerListObjectObserver:self];
63 //Register as a chat observer (for unviewed content)
64 [[adium chatController] registerChatObserver:self];
66 [[adium notificationCenter] addObserver:self
67 selector:@selector(chatClosed:)
72 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_THEME];
73 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
76 image1 = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
77 image2 = [[NSImage alloc] initWithSize:NSMakeSize(128,128)];
79 //Install our contact alert
80 [[adium contactAlertsController] registerActionID:DOCK_OVERLAY_ALERT_IDENTIFIER withHandler:self];
83 - (void)uninstallPlugin
85 [[adium contactController] unregisterListObjectObserver:self];
86 [[adium chatController] unregisterChatObserver:self];
87 [[adium notificationCenter] removeObserver:self];
88 [[adium preferenceController] unregisterPreferenceObserver:self];
92 * @brief Short description
93 * @result A short localized description of the action
95 - (NSString *)shortDescriptionForActionID:(NSString *)actionID
97 return DOCK_OVERLAY_ALERT_SHORT;
101 * @brief Long description
102 * @result A longer localized description of the action which should take into account the details dictionary as appropraite.
104 - (NSString *)longDescriptionForActionID:(NSString *)actionID withDetails:(NSDictionary *)details
106 return DOCK_OVERLAY_ALERT_LONG;
112 - (NSImage *)imageForActionID:(NSString *)actionID
115 return [NSImage imageNamed:@"DockAlert" forClass:[self class]];
119 * @brief Details pane
120 * @result An <tt>AIModularPane</tt> to use for configuring this action, or nil if no configuration is possible.
122 - (AIModularPane *)detailsPaneForActionID:(NSString *)actionID
128 * @brief Perform an action
130 * @param actionID The ID of the action to perform
131 * @param listObject The listObject associated with the event triggering the action. It may be nil
132 * @param details If set by the details pane when the action was created, the details dictionary for this particular action
133 * @param eventID The eventID which triggered this action
134 * @param userInfo Additional information associated with the event; userInfo's type will vary with the actionID.
136 - (BOOL)performActionID:(NSString *)actionID forListObject:(AIListObject *)listObject withDetails:(NSDictionary *)details triggeringEventID:(NSString *)eventID userInfo:(id)userInfo
138 BOOL isMessageEvent = [[adium contactAlertsController] isMessageEvent:eventID];
140 if (isMessageEvent) {
143 if ((chat = [userInfo objectForKey:@"AIChat"]) &&
144 (chat != [[adium interfaceController] activeChat]) &&
145 (![overlayObjectsArray containsObjectIdenticalTo:chat])) {
146 [overlayObjectsArray addObject:chat];
148 //Wait until the next run loop so this event is done processing (and our unviewed content count is right)
149 [self performSelector:@selector(_setOverlay)
153 /* The chat observer method is responsible for removing this overlay later */
156 } else if (listObject) {
157 NSTimer *removeTimer;
159 //Clear any current timer for this object o ahve its overlay removed
160 if ((removeTimer = [listObject statusObjectForKey:@"DockOverlayRemoveTimer"])) [removeTimer invalidate];
162 //Add a timer to remove this overlay
163 removeTimer = [NSTimer scheduledTimerWithTimeInterval:5
165 selector:@selector(removeDockOverlay:)
168 [listObject setStatusObject:removeTimer
169 forKey:@"DockOverlayRemoveTimer"
172 if (![overlayObjectsArray containsObject:listObject]) {
173 [overlayObjectsArray addObject:listObject];
176 //Wait until the next run loop so this event is done processing
177 [self performSelector:@selector(_setOverlay)
185 - (void)removeDockOverlay:(NSTimer *)removeTimer
187 AIListObject *inObject = [removeTimer userInfo];
189 [overlayObjectsArray removeObjectIdenticalTo:inObject];
191 [inObject setStatusObject:nil
192 forKey:@"DockOverlayRemoveTimer"
198 - (void)chatClosed:(NSNotification *)notification
200 AIChat *chat = [notification object];
202 [overlayObjectsArray removeObjectIdenticalTo:chat];
208 * @brief Allow multiple actions?
210 * If this method returns YES, every one of this action associated with the triggering event will be executed.
211 * If this method returns NO, only the first will be.
213 * Don't allow multiple dock actions to occur. While a series of "Bounce every 5 seconds," "Bounce every 10 seconds,"
214 * and so on actions could be combined sanely, a series of "Bounce once" would make the dock go crazy.
216 - (BOOL)allowMultipleActionsWithID:(NSString *)actionID
221 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
222 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
224 if ([group isEqualToString:PREF_GROUP_LIST_THEME]) {
225 //Grab colors from status coloring plugin's prefs
226 [self flushPreferenceColorCache];
227 signedOffColor = [[[prefDict objectForKey:KEY_SIGNED_OFF_COLOR] representedColor] retain];
228 signedOnColor = [[[prefDict objectForKey:KEY_SIGNED_ON_COLOR] representedColor] retain];
229 unviewedContentColor = [[[prefDict objectForKey:KEY_UNVIEWED_COLOR] representedColor] retain];
231 backSignedOffColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_OFF_COLOR] representedColor] retain];
232 backSignedOnColor = [[[prefDict objectForKey:KEY_LABEL_SIGNED_ON_COLOR] representedColor] retain];
233 backUnviewedContentColor = [[[prefDict objectForKey:KEY_LABEL_UNVIEWED_COLOR] representedColor] retain];
235 } else if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
236 if (!key || [key isEqualToString:KEY_ANIMATE_DOCK_ICON]) {
237 BOOL newShouldAnimate = [[prefDict objectForKey:KEY_ANIMATE_DOCK_ICON] boolValue];
238 if (newShouldAnimate != shouldAnimate) {
239 shouldAnimate = newShouldAnimate;
241 //Redo our overlay to respect our new preference
242 if (!firstTime) [self _setOverlay];
249 - (void)flushPreferenceColorCache
251 [signedOffColor release]; signedOffColor = nil;
252 [signedOnColor release]; signedOnColor = nil;
253 [unviewedContentColor release]; unviewedContentColor = nil;
254 [backSignedOffColor release]; backSignedOffColor = nil;
255 [backSignedOnColor release]; backSignedOnColor = nil;
256 [backUnviewedContentColor release]; backUnviewedContentColor = nil;
259 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
261 if ([inObject isKindOfClass:[AIAccount class]]) {
262 //When an account signs on or off, force an overlay update as it may have silently changed
264 if ([inModifiedKeys containsObject:@"Online"]) {
265 NSEnumerator *enumerator = [[[overlayObjectsArray copy] autorelease] objectEnumerator];
266 AIListObject *listObject;
267 BOOL madeChanges = NO;
269 while ((listObject = [enumerator nextObject])) {
270 if (([listObject respondsToSelector:@selector(account)]) &&
271 ([(id)listObject account] == inObject) &&
272 ([overlayObjectsArray containsObjectIdenticalTo:listObject])) {
273 [overlayObjectsArray removeObject:listObject];
278 if (madeChanges) [self _setOverlay];
286 * @brief When a chat no longer has unviewed content, remove it from display
288 - (NSSet *)updateChat:(AIChat *)inChat keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
290 if (inModifiedKeys == nil || [inModifiedKeys containsObject:KEY_UNVIEWED_CONTENT]) {
292 if (![inChat unviewedContentCount]) {
293 if ([overlayObjectsArray containsObjectIdenticalTo:inChat]) {
294 [overlayObjectsArray removeObjectIdenticalTo:inChat];
306 //Remove & release the current overlay state
308 [[adium dockController] removeIconStateNamed:@"ContactStatusOverlay"];
309 [overlayState release]; overlayState = nil;
312 //Create & set the new overlay state
313 if ([overlayObjectsArray count] != 0) {
316 overlayState = [[AIIconState alloc] initWithImages:[NSArray arrayWithObjects:[self overlayImageFlash:NO], [self overlayImageFlash:YES], nil]
321 overlayState = [[AIIconState alloc] initWithImage:[self overlayImageFlash:NO]
325 [[adium dockController] setIconState:overlayState named:@"ContactStatusOverlay"];
330 - (NSImage *)overlayImageFlash:(BOOL)flash
332 NSEnumerator *enumerator;
333 ESObjectWithStatus *object;
335 NSParagraphStyle *paragraphStyle;
339 NSImage *image = (flash ? image1 : image2);
341 //Pre-calc some sizes
342 dockIconScale = 1- [[adium dockController] dockIconScale];
343 iconHeight = (SMALLESTRADIUS + (RADIUSRANGE * dockIconScale));
346 bottom = top - iconHeight;
348 //Set up the string details
349 font = [NSFont boldSystemFontOfSize:(SMALLESTFONTSIZE + (FONTSIZERANGE * dockIconScale))];
350 paragraphStyle = [NSParagraphStyle styleWithAlignment:NSCenterTextAlignment lineBreakMode:NSLineBreakByClipping];
354 [[NSColor clearColor] set];
355 NSRectFillUsingOperation(NSMakeRect(0, 0, 128, 128), NSCompositeCopy);
357 //Draw overlays for each contact
358 enumerator = [overlayObjectsArray reverseObjectEnumerator];
359 while ((object = [enumerator nextObject]) && !(top < 0) && bottom < 128) {
360 float left, right, arcRadius, stringInset;
362 NSColor *backColor = nil, *textColor = nil, *borderColor = nil;
364 //Create the pill frame
365 arcRadius = (iconHeight / 2.0f);
366 stringInset = (iconHeight / 4.0f);
367 left = 1 + arcRadius;
368 right = 127 - arcRadius;
370 path = [NSBezierPath bezierPath];
371 [path setLineWidth:((iconHeight/2.0) * 0.13333f)];
373 [path moveToPoint: NSMakePoint(left, top)];
374 [path lineToPoint: NSMakePoint(right, top)];
377 [path appendBezierPathWithArcWithCenter:NSMakePoint(right, top - arcRadius)
382 [path lineToPoint: NSMakePoint(right + arcRadius, bottom + arcRadius)];
383 [path appendBezierPathWithArcWithCenter:NSMakePoint(right, bottom + arcRadius)
390 [path moveToPoint: NSMakePoint(right, bottom)];
391 [path lineToPoint: NSMakePoint(left, bottom)];
394 [path appendBezierPathWithArcWithCenter:NSMakePoint(left, bottom + arcRadius)
399 [path lineToPoint: NSMakePoint(left - arcRadius, top - arcRadius)];
400 [path appendBezierPathWithArcWithCenter:NSMakePoint(left, top - arcRadius) radius:arcRadius startAngle:180 endAngle:90 clockwise:YES];
404 if (!([contact unviewedContentCount] && flash)) {
405 backColor = [[contact displayArrayForKey:@"Label Color"] averageColor];
406 textColor = [[contact displayArrayForKey:@"Text Color"] averageColor];
410 if ([object statusObjectForKey:KEY_UNVIEWED_CONTENT]) { //Unviewed
412 backColor = [NSColor whiteColor];
413 textColor = [NSColor blackColor];
415 backColor = backUnviewedContentColor;
416 textColor = unviewedContentColor;
418 } else if ([object integerStatusObjectForKey:@"Signed On"]) { //Signed on
419 backColor = backSignedOnColor;
420 textColor = signedOnColor;
422 } else if ([object integerStatusObjectForKey:@"Signed Off"]) { //Signed off
423 backColor = backSignedOffColor;
424 textColor = signedOffColor;
429 backColor = [NSColor whiteColor];
432 textColor = [NSColor blackColor];
435 //Lighten/Darken the back color slightly
436 if ([backColor colorIsDark]) {
437 backColor = [backColor darkenBy:-0.15];
438 borderColor = [backColor darkenBy:-0.3];
440 backColor = [backColor darkenBy:0.15];
441 borderColor = [backColor darkenBy:0.3];
450 //Get the object's display name
451 [[object displayName] drawInRect:NSMakeRect(0 + stringInset, bottom + 1, 128 - (stringInset * 2), top - bottom)
452 withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]];
454 nameString = [[[NSAttributedString alloc] initWithString:[contact displayName] attributes:[NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName, paragraphStyle, NSParagraphStyleAttributeName, textColor, NSForegroundColorAttributeName, nil]] autorelease];
455 [nameString drawInRect:NSMakeRect(0 + stringInset, bottom + 1, 128 - (stringInset * 2), top - bottom)];*/
457 //Move down to the next pill
458 top -= (iconHeight + 7.0 * dockIconScale);
459 bottom = top - iconHeight;